package com.ruoyi.common.core.utils.ip;

import com.ruoyi.common.core.utils.ServletUtils;
import com.ruoyi.common.core.utils.StringUtils;
import jakarta.servlet.http.HttpServletRequest;

import java.net.InetAddress;
import java.net.UnknownHostException;

/**
 * 获取IP方法
 *
 * @author ruoyi
 */
public class IpUtils {
    /**
     * 该函数定义了一个名为REGX_0_255的静态常量，其值为一个正则表达式字符串。该正则表达式用于匹配一个合法的IPv4地址的范围，即0到255之间的整数。具体来说，该正则表达式由以下几个部分组成：
     * (25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]\\d|\\d)：匹配一个合法的IPv4地址的范围，即0到255之间的整数。具体来说，该部分由以下几个部分组成：
     * 25[0-5]：匹配250到255之间的整数。
     * 2[0-4]\\d：匹配200到249之间的整数。
     * 1\\d{2}：匹配100到199之间的整数。
     * [1-9]\\d：匹配10到99之间的整数。
     * \\d：匹配0到9之间的整数。
     * 因此，该函数的作用是定义一个用于匹配IPv4地址范围的正则表达式字符串。
     */
    public final static String REGX_0_255 = "(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]\\d|\\d)";
    /**
     * 该函数定义了一个名为REGX_IP的静态常量，其值为一个正则表达式字符串，用于匹配IP地址。
     * 该正则表达式由三个部分组成，每个部分由一个或多个0到255之间的数字组成，并且每个部分之间用点号分隔。
     */
    public final static String REGX_IP = "((" + REGX_0_255 + "\\.){3}" + REGX_0_255 + ")";
    /**
     * 这个函数定义了一个静态的final字符串常量REGX_IP_SEG，它是由REGX_IP和\\-拼接而成的。REGX_IP是一个正则表达式，用于匹配IP地址，\\-是一个转义字符，表示一个减号。因此，REGX_IP_SEG是一个用于匹配IP地址段的正则表达式。
     */
    public final static String REGX_IP_SEG = "(" + REGX_IP + "\\-" + REGX_IP + ")";
    // 匹配网段
    /**
     * 该函数定义了一个名为REGX_IP_WILDCARD的静态常量，它是一个正则表达式字符串，用于匹配IP地址中的通配符。该正则表达式可以匹配以下四种类型的IP地址：
     * 通配符为""的IP地址，例如".192.168.1.1"。
     * 通配符为"."的IP地址，例如"192.168.1.*"。
     * 通配符为"."的IP地址，例如".192.168.1"。
     * 通配符为".."的IP地址，例如"192.168..1"。
     * 该函数的实现比较复杂，因为它使用了多个子表达式和嵌套的括号来构建正则表达式。
     */
    public final static String REGX_IP_WILDCARD = "(((\\*\\.){3}\\*)|(" + REGX_0_255 + "(\\.\\*){3})|(" + REGX_0_255 + "\\." + REGX_0_255 + ")(\\.\\*){2}" + "|((" + REGX_0_255 + "\\.){3}\\*))";

    /**
     * 获取客户端IP
     *
     * @return IP地址
     */
    public static String getIpAddr() {
        HttpServletRequest request = ServletUtils.getRequest();
        if (request == null) {
            return "unknown";  // 未知
        }
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Forwarded-For");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }

        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }

        return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : getMultistageReverseProxyIp(ip);
    }

    /**
     * 检查是否为内部IP地址
     *
     * @param ip IP地址
     * @return 结果
     */
    public static boolean internalIp(String ip) {
        byte[] addr = textToNumericFormatV4(ip);
        return internalIp(addr) || "127.0.0.1".equals(ip);
    }

    /**
     * 检查是否为内部IP地址
     * <p>
     * 该函数用于检查给定的byte数组是否为内部IP地址。内部IP地址是指私有IP地址，
     * 包括10.x.x.x/8、172.16.x.x/12和192.168.x.x/16。
     * 函数首先检查输入的byte数组是否为空或长度小于2，如果是，则返回true。
     * 然后，函数根据byte数组的前两个字节判断是否为内部IP地址。
     * 如果前两个字节是10.x.x.x/8，则返回true；如果前两个字节是172.16.x.x/12，
     * 则判断第三个字节是否在SECTION_3和SECTION_4之间，如果是，则返回true；
     * 如果前两个字节是192.168.x.x/16，则判断第三个字节是否为SECTION_6，如果是，
     * 则返回true。如果以上条件都不满足，则返回false。
     *
     * @param addr byte地址
     * @return 结果
     */
    private static boolean internalIp(byte[] addr) {
        if (StringUtils.isNull(addr) || addr.length < 2) {
            return true;
        }
        final byte b0 = addr[0];
        final byte b1 = addr[1];
        // 10.x.x.x/8
        final byte SECTION_1 = 0x0A;
        // 172.16.x.x/12
        final byte SECTION_2 = (byte) 0xAC;
        final byte SECTION_3 = (byte) 0x10;
        final byte SECTION_4 = (byte) 0x1F;
        // 192.168.x.x/16
        final byte SECTION_5 = (byte) 0xC0;
        final byte SECTION_6 = (byte) 0xA8;
        switch (b0) {
            case SECTION_1:
                return true;
            case SECTION_2:
                if (b1 >= SECTION_3 && b1 <= SECTION_4) {
                    return true;
                }
            case SECTION_5:
                switch (b1) {
                    case SECTION_6:
                        return true;
                }
            default:
                return false;
        }
    }

    /**
     * 将IPv4地址转换成字节
     * <p>
     * <p>
     * 该函数的功能是将IPv4地址转换成字节。函数接受一个IPv4地址的字符串作为参数，返回一个包含4个字节的数组。
     * 函数首先检查输入的字符串是否为空，如果为空则返回null。
     * 然后，函数将输入的字符串按照点号"."进行分割，得到一个字符串数组。
     * 接下来，函数根据字符串数组的长度进行不同的处理：
     * 如果字符串数组的长度为1，表示输入的IPv4地址只包含一个数字。函数将该数字转换成32位的长整型数，并根据位运算的规则将其转换成4个字节。如果转换后的数字超出了IPv4地址的范围，则返回null。
     * 如果字符串数组的长度为2，表示输入的IPv4地址包含两个数字。函数将前两个数字转换成16位和24位的整型数，并根据位运算的规则将其转换成4个字节。如果转换后的数字超出了IPv4地址的范围，则返回null。
     * 如果字符串数组的长度为3，表示输入的IPv4地址包含三个数字。函数将前两个数字转换成8位和16位的整型数，并根据位运算的规则将其转换成4个字节。如果转换后的数字超出了IPv4地址的范围，则返回null。
     * 如果字符串数组的长度为4，表示输入的IPv4地址包含四个数字。函数将每个数字转换成8位的整型数，并根据位运算的规则将其转换成4个字节。
     * 最后，如果在转换过程中出现异常（例如转换的数字超出了整型数的范围），则返回null。
     * 如果所有的转换都成功，函数返回包含4个字节的数组。
     *
     * @param text IPv4地址
     * @return byte 字节
     */
    public static byte[] textToNumericFormatV4(String text) {
        if (text.length() == 0) {
            return null;
        }

        byte[] bytes = new byte[4];
        String[] elements = text.split("\\.", -1);
        try {
            long l;
            int i;
            switch (elements.length) {
                case 1:
                    l = Long.parseLong(elements[0]);
                    if ((l < 0L) || (l > 4294967295L)) {
                        return null;
                    }
                    bytes[0] = (byte) (int) (l >> 24 & 0xFF);
                    bytes[1] = (byte) (int) ((l & 0xFFFFFF) >> 16 & 0xFF);
                    bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF);
                    bytes[3] = (byte) (int) (l & 0xFF);
                    break;
                case 2:
                    l = Integer.parseInt(elements[0]);
                    if ((l < 0L) || (l > 255L)) {
                        return null;
                    }
                    bytes[0] = (byte) (int) (l & 0xFF);
                    l = Integer.parseInt(elements[1]);
                    if ((l < 0L) || (l > 16777215L)) {
                        return null;
                    }
                    bytes[1] = (byte) (int) (l >> 16 & 0xFF);
                    bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF);
                    bytes[3] = (byte) (int) (l & 0xFF);
                    break;
                case 3:
                    for (i = 0; i < 2; ++i) {
                        l = Integer.parseInt(elements[i]);
                        if ((l < 0L) || (l > 255L)) {
                            return null;
                        }
                        bytes[i] = (byte) (int) (l & 0xFF);
                    }
                    l = Integer.parseInt(elements[2]);
                    if ((l < 0L) || (l > 65535L)) {
                        return null;
                    }
                    bytes[2] = (byte) (int) (l >> 8 & 0xFF);
                    bytes[3] = (byte) (int) (l & 0xFF);
                    break;
                case 4:
                    for (i = 0; i < 4; ++i) {
                        l = Integer.parseInt(elements[i]);
                        if ((l < 0L) || (l > 255L)) {
                            return null;
                        }
                        bytes[i] = (byte) (int) (l & 0xFF);
                    }
                    break;
                default:
                    return null;
            }
        } catch (NumberFormatException e) {
            return null;
        }
        return bytes;
    }

    /**
     * 获取IP地址
     *
     * @return 本地IP地址
     */
    public static String getHostIp() {
        try {
            return InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException e) {
            throw new RuntimeException(e.getMessage(), e);
        }
    }

    /**
     * 获取主机名
     *
     * @return 本地主机名
     */
    public static String getHostName() {
        try {
            return InetAddress.getLocalHost().getHostName();
        } catch (UnknownHostException e) {
        }
        return "未知";
    }

    /**
     * 从多级反向代理中获得第一个非unknown IP地址
     * <p>
     * 该函数的功能是从多级反向代理中获取第一个非unknown的IP地址。
     * 如果输入的IP地址是一个包含多个IP地址的字符串，函数会遍历这些IP地址，
     * 找到第一个不是unknown的IP地址并返回。如果输入的IP地址是unknown或者只有一个unknown的IP地址，
     * 则返回null。函数还对输入的IP地址进行了长度限制，确保返回的IP地址长度不超过255个字符。
     *
     * @param ip 获得的IP地址
     * @return 第一个非unknown IP地址
     */
    public static String getMultistageReverseProxyIp(String ip) {
        // 多级反向代理检测
        if (ip != null && ip.indexOf(",") > 0) {
            final String[] ips = ip.trim().split(",");
            for (String subIp : ips) {
                if (false == isUnknown(subIp)) {
                    ip = subIp;
                    break;
                }
            }
        }
        return StringUtils.substring(ip, 0, 255);
    }

    /**
     * 检测给定字符串是否为未知，多用于检测HTTP请求相关
     *
     * @param checkString 被检测的字符串
     * @return 是否未知
     */
    public static boolean isUnknown(String checkString) {
        return StringUtils.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString);
    }

    /**
     * 是否为IP
     */
    public static boolean isIP(String ip) {
        return StringUtils.isNotBlank(ip) && ip.matches(REGX_IP);
    }

    /**
     * 是否为IP，或 *为间隔的通配符地址
     */
    public static boolean isIpWildCard(String ip) {
        return StringUtils.isNotBlank(ip) && ip.matches(REGX_IP_WILDCARD);
    }

    /**
     * 检测参数是否在ip通配符里
     * <p>
     * 该函数用于检测一个IP地址是否在给定的IP通配符中。
     * 函数接受两个参数：ipWildCard表示IP通配符，
     * ip表示要检测的IP地址。
     * 函数首先将IP通配符和IP地址按照点号分割成数组，然后逐段比较。
     * 如果通配符中某一段为*，则表示该段可以匹配任意值。
     * 如果通配符中某一段不为*且与IP地址对应段不相等，则表示不匹配。
     * 如果所有段都匹配，则返回true，否则返回false。
     *
     * @param ipWildCard ip通配符
     * @param ip         ip
     */
    public static boolean ipIsInWildCardNoCheck(String ipWildCard, String ip) {
        String[] s1 = ipWildCard.split("\\.");
        String[] s2 = ip.split("\\.");
        boolean isMatchedSeg = true;
        for (int i = 0; i < s1.length && !s1[i].equals("*"); i++) {
            if (!s1[i].equals(s2[i])) {
                isMatchedSeg = false;
                break;
            }
        }
        return isMatchedSeg;
    }

    /**
     * 是否为特定格式如:“10.10.10.1-10.10.10.99”的ip段字符串
     */
    public static boolean isIPSegment(String ipSeg) {
        return StringUtils.isNotBlank(ipSeg) && ipSeg.matches(REGX_IP_SEG);
    }

    /**
     * 判断ip是否在指定网段中
     * 该函数用于判断一个IP地址是否在指定的网段中。函数接受两个参数，一个是网段地址（iparea），一个是待判断的IP地址（ip）。函数首先根据网段地址中的"-"符号获取网段的起始地址和结束地址，然后将网段地址和待判断的IP地址分别拆分成四个数字。接下来，将拆分后的数字转换为整数，并将其转换为长整型。然后，通过位运算将长整型转换为IPv4地址。最后，比较待判断的IP地址和网段的起始地址和结束地址，如果待判断的IP地址在网段的起始地址和结束地址之间，则返回true，否则返回false。
     *
     * @param iparea 网段地址
     * @param ip     待判断的IP地址
     * @return boolean
     */
    public static boolean ipIsInNetNoCheck(String iparea, String ip) {
        int idx = iparea.indexOf('-');
        String[] sips = iparea.substring(0, idx).split("\\.");
        String[] sipe = iparea.substring(idx + 1).split("\\.");
        String[] sipt = ip.split("\\.");
        long ips = 0L, ipe = 0L, ipt = 0L;
        for (int i = 0; i < 4; ++i) {
            ips = ips << 8 | Integer.parseInt(sips[i]);
            ipe = ipe << 8 | Integer.parseInt(sipe[i]);
            ipt = ipt << 8 | Integer.parseInt(sipt[i]);
        }
        if (ips > ipe) {
            long t = ips;
            ips = ipe;
            ipe = t;
        }
        return ips <= ipt && ipt <= ipe;
    }

    /**
     * 校验ip是否符合过滤串规则
     * <p>
     * 该函数用于校验一个IP地址是否符合给定的过滤串规则。过滤串是一个字符串，可以包含多个IP地址，每个地址之间用分号分隔。过滤串中的IP地址可以使用通配符"*"来表示通配，也可以使用网段形式如"10.10.10.1-10.10.10.99"。
     * 函数的参数为过滤串和待校验的IP地址，返回一个布尔值表示是否匹配。
     * 函数首先判断过滤串和待校验的IP地址是否为空，如果为空则直接返回false。
     * 然后将过滤串按照分号分割成多个IP地址，并遍历这些IP地址。
     * 对于每个IP地址，函数会调用isIP()方法判断是否为一个合法的IP地址，如果是则继续判断是否与待校验的IP地址相等。
     * 如果isIP()返回true且地址相等，则返回true。
     * 如果isIP()返回true但地址不相等，则调用isIpWildCard()方法判断该IP地址是否为通配符。
     * 如果是通配符，则调用ipIsInWildCardNoCheck()方法判断待校验的IP地址是否在通配符范围内。
     * 如果isIpWildCard()返回true且待校验的IP地址在通配符范围内，则返回true。
     * 如果isIP()和isIpWildCard()都返回false，则调用isIPSegment()方法判断该IP地址是否为网段。
     * 如果是网段，则调用ipIsInNetNoCheck()方法判断待校验的IP地址是否在网段内。
     * 如果isIPSegment()返回true且待校验的IP地址在网段内，则返回true。
     * 如果遍历完所有IP地址都没有匹配的，则返回false。
     *
     * @param filter 过滤IP列表,支持后缀'*'通配,支持网段如:`10.10.10.1-10.10.10.99`
     * @param ip     校验IP地址
     * @return boolean 结果
     */
    public static boolean isMatchedIp(String filter, String ip) {
        if (StringUtils.isEmpty(filter) || StringUtils.isEmpty(ip)) {
            return false;
        }
        String[] ips = filter.split(";");
        for (String iStr : ips) {
            if (isIP(iStr) && iStr.equals(ip)) {
                return true;
            } else if (isIpWildCard(iStr) && ipIsInWildCardNoCheck(iStr, ip)) {
                return true;
            } else if (isIPSegment(iStr) && ipIsInNetNoCheck(iStr, ip)) {
                return true;
            }
        }
        return false;
    }
}
